/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2021 Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/cpu/linux.h>
#include <grub/linux.h>
#include <grub/fdt.h>
#include <grub/efi/efi.h>
#include <grub/elfload.h>
#include <grub/cpu/relocator.h>
#include <grub/efi/memory.h>
#include <grub/efi/graphics_output.h>

#define GRUB_EFI_MMAP_NR_SLACK_SLOTS 8

#define GRUB_ADDRESS_TYPE_SYSRAM	1
#define GRUB_ADDRESS_TYPE_RESERVED	2
#define GRUB_ADDRESS_TYPE_ACPI		3
#define GRUB_ADDRESS_TYPE_NVS		4
#define GRUB_ADDRESS_TYPE_PMEM		5

#define GRUB_EFI_LOONGSON_BPI_TABLE_GUID	\
    { 0x4660f721, 0x2ec5, 0x416a, \
	{ 0x89, 0x9a, 0x43, 0x18, 0x02, 0x50, 0xa0, 0xc9 } \
    }

#define GRUB_EFI_LARCH_SCREEN_INFO_GUID \
  { 0x07fd51a6, 0x9532, 0x926f, \
    { 0x51, 0xdc, 0x6a, 0x63, 0x60, 0x2f, 0x84, 0xb4 } \
  }

#define GRUB_EFI_LARCH_CONSOLE_OUT_DEVICE_GUID \
  { 0xd3b36f2c, 0xd551, 0x11d4,  \
    { 0x9a, 0x46, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d } \
  }

#define GRUB_EFI_LARCH_BOOT_MEMMAP_GUID \
  { 0x800f683f, 0xd08b, 0x423a,  \
    { 0xa2, 0x93, 0x96, 0x5c, 0x3c, 0x6f, 0xe2, 0xb4 } \
  }

#define GRUB_EFI_LARCH_INITRD_MEDIA_GUID \
  { 0x5568e427, 0x68fc, 0x4f3d,  \
    { 0xac, 0x74, 0xca, 0x55, 0x52, 0x31, 0xcc, 0x68 } \
  }

#define GRUB_EFI_SCREEN_INFO_GUID \
  { 0xe03fc20a, 0x85dc, 0x406e,  \
    { 0xb9, 0x0e, 0x4a, 0xb5, 0x02, 0x37, 0x1d, 0x95 } \
  }

static struct grub_relocator *relocator;
static grub_guid_t compat_screen_info_guid = GRUB_EFI_LARCH_SCREEN_INFO_GUID;
static grub_guid_t screen_info_guid = GRUB_EFI_SCREEN_INFO_GUID;

void grub_linux_loongarch_elf_relocator_unload (void)
{
  grub_relocator_unload (relocator);
}

static void
find_bits (unsigned long mask, grub_efi_uint8_t *pos, grub_efi_uint8_t *size)
{
  grub_efi_uint8_t first, len;

  first = 0;
  len = 0;

  if (mask)
    {
      while (!(mask & 0x1))
        {
    mask = mask >> 1;
    first++;
        }

      while (mask & 0x1)
        {
    mask = mask >> 1;
    len++;
        }
    }

  *pos = first;
  *size = len;
}

static void
setup_pixel_info (struct screen_info *si, grub_efi_uint32_t pixels_per_scan_line,
                  struct grub_efi_gop_pixel_bitmask pixel_info, int pixel_format)
{
  if (pixel_format == GRUB_EFI_GOT_RGBA8)
    {
      si->lfb_depth = 32;
      si->lfb_linelength = pixels_per_scan_line * 4;
      si->red_size = 8;
      si->red_pos = 0;
      si->green_size = 8;
      si->green_pos = 8;
      si->blue_size = 8;
      si->blue_pos = 16;
      si->rsvd_size = 8;
      si->rsvd_pos = 24;
    }
  else if (pixel_format == GRUB_EFI_GOT_BGRA8)
    {
      si->lfb_depth = 32;
      si->lfb_linelength = pixels_per_scan_line * 4;
      si->red_size = 8;
      si->red_pos = 16;
      si->green_size = 8;
      si->green_pos = 8;
      si->blue_size = 8;
      si->blue_pos = 0;
      si->rsvd_size = 8;
      si->rsvd_pos = 24;
    }
  else if (pixel_format == GRUB_EFI_GOT_BITMASK)
    {
      find_bits(pixel_info.r, &si->red_pos, &si->red_size);
      find_bits(pixel_info.g, &si->green_pos, &si->green_size);
      find_bits(pixel_info.b, &si->blue_pos, &si->blue_size);
      find_bits(pixel_info.a, &si->rsvd_pos, &si->rsvd_size);
      si->lfb_depth = si->red_size + si->green_size +
                      si->blue_size + si->rsvd_size;
      si->lfb_linelength = (pixels_per_scan_line * si->lfb_depth) / 8;
    }
  else
    {
      si->lfb_depth = 4;
      si->lfb_linelength = si->lfb_width / 2;
      si->red_size = 0;
      si->red_pos = 0;
      si->green_size = 0;
      si->green_pos = 0;
      si->blue_size = 0;
      si->blue_pos = 0;
      si->rsvd_size = 0;
      si->rsvd_pos = 0;
    }
}

static struct screen_info *
alloc_screen_info (void)
{
  grub_efi_status_t status;
  grub_efi_boot_services_t *b;
  struct screen_info *si;

  b = grub_efi_system_table->boot_services;
  status = b->allocate_pool (GRUB_EFI_RUNTIME_SERVICES_DATA,
                           sizeof(*si), (void**)&si);
  if (status != GRUB_EFI_SUCCESS)
    return NULL;

  grub_memset((void *)si, 0, sizeof(struct screen_info));

  status = b->install_configuration_table (&compat_screen_info_guid, si);
  if (status != GRUB_EFI_SUCCESS)
    goto free_mem;

  status = b->install_configuration_table (&screen_info_guid, si);
  if (status == GRUB_EFI_SUCCESS)
    return si;

free_table:
  b->install_configuration_table (&compat_screen_info_guid, NULL);
free_mem:
  b->free_pool (si);

  return NULL;
}

static struct screen_info *
setup_screen_info (void)
{
  grub_efi_boot_services_t *b;
  grub_efi_handle_t gop_handle;
  struct screen_info *si = NULL;
  struct grub_efi_gop *gop, *first_gop;
  grub_efi_handle_t *handles;
  grub_efi_uintn_t num_handles, i;
  grub_guid_t graphics_output_guid = GRUB_EFI_GOP_GUID;
  grub_efi_uint16_t width, height;
  grub_efi_uint32_t ext_lfb_base, pixels_per_scan_line;
  grub_efi_uint64_t fb_base;
  struct grub_efi_gop_pixel_bitmask pixel_info;
  grub_efi_gop_pixel_format_t pixel_format;

  si = alloc_screen_info();
  if (!si)
    return NULL;

  handles = grub_efi_locate_handle (GRUB_EFI_BY_PROTOCOL,
                    &graphics_output_guid, NULL, &num_handles);
  if (!handles || num_handles == 0)
    goto free_screen_info;

  gop = NULL;
  first_gop = NULL;

  for (i = 0; i < num_handles; i++)
    {
      struct grub_efi_gop_mode *mode;
      struct grub_efi_gop_mode_info *info = NULL;
      grub_guid_t conout_proto = GRUB_EFI_LARCH_CONSOLE_OUT_DEVICE_GUID;
      void *dummy = NULL;
      grub_efi_uint8_t conout_found = 0;
      grub_efi_uint64_t current_fb_base;

      gop_handle = handles[i];
      gop = grub_efi_open_protocol (gop_handle, &graphics_output_guid,
            GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

      dummy = grub_efi_open_protocol (gop_handle, &conout_proto,
                GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
      if (dummy != NULL)
        conout_found = 1;

      mode = gop->mode;
      info = mode->info;
      current_fb_base = mode->fb_base;

      if ((!first_gop || conout_found) &&
        info->pixel_format != GRUB_EFI_GOT_BLT_ONLY)
        {
    /*
    * Systems that use the UEFI Console Splitter may
    * provide multiple GOP devices, not all of which are
    * backed by real hardware. The workaround is to search
    * for a GOP implementing the ConOut protocol, and if
    * one isn't found, to just fall back to the first GOP.
    */
    width = info->width;
    height = info->height;
    pixel_format = info->pixel_format;
    pixel_info = info->pixel_bitmask;
    pixels_per_scan_line = info->pixels_per_scanline;
    fb_base = current_fb_base;

    /*
    * Once we've found a GOP supporting ConOut,
    * don't bother looking any further.
    */
    first_gop = gop;
    if (conout_found)
      break;
        }
    }

  /* Did we find any GOPs? */
  if (!first_gop)
    goto free_screen_info;

  /* EFI framebuffer */
  si->orig_video_isVGA = GRUB_VIDEO_TYPE_EFI;

  si->lfb_width = width;
  si->lfb_height = height;
  si->lfb_base = fb_base;
  grub_dprintf ("loongson", "Screen info fb base: 0x%"PRIxGRUB_UINT32_T"\n",
                si->lfb_base);

  ext_lfb_base = (grub_uint64_t)fb_base >> 32;
  if (ext_lfb_base) {
    si->capabilities |= GRUB_VIDEO_CAPABILITY_64BIT_BASE;
    si->ext_lfb_base = ext_lfb_base;
  }
  si->pages = 1;

  setup_pixel_info(si, pixels_per_scan_line, pixel_info, pixel_format);

  si->lfb_size = si->lfb_linelength * si->lfb_height;
  si->capabilities |= GRUB_VIDEO_CAPABILITY_SKIP_QUIRKS;

  return si;

free_screen_info:
  b = grub_efi_system_table->boot_services;
  b->install_configuration_table (&compat_screen_info_guid, NULL);
  b->install_configuration_table (&screen_info_guid, NULL);
  if (si)
    b->free_pool (si);

  grub_dprintf ("loongson", "No screen info\n");
  return NULL;
}

static grub_err_t
allocate_memmap_and_exit_boot (struct linux_loongarch64_kernel_params *kernel_params)
{
  grub_err_t err;
  grub_efi_status_t status;
  grub_efi_uintn_t mmap_size, desc_size, size;
  grub_efi_uint32_t desc_version;
  grub_efi_memory_descriptor_t *mmap_buf;
  grub_efi_boot_services_t *b;
  struct efi_boot_memmap *m, tmp;
  grub_guid_t boot_memmap_guid = GRUB_EFI_LARCH_BOOT_MEMMAP_GUID;

  setup_screen_info();

  grub_dprintf ("loongson", "ramdisk_addr:0x%"PRIxGRUB_UINT64_T", \
          size:0x%"PRIxGRUB_UINT64_T"\n",
          kernel_params->ramdisk_addr,
          kernel_params->ramdisk_size);

  /* Set initrd info to system table*/
  err = grub_loongarch_setup_initrd_params();
  if (err != GRUB_ERR_NONE)
    {
      grub_error(GRUB_ERR_IO, "failed to install initrd media");
      return err;
    }

  tmp.map_size = 0;
  status = grub_efi_get_memory_map (&tmp.map_size, NULL, &tmp.map_key,
                    &tmp.desc_size, &tmp.desc_ver);
  if (status != 0) {
    grub_error (GRUB_ERR_IO, "cannot get memory map");
    goto uninstall_initrd_table;
  }
  size = tmp.map_size + tmp.desc_size * GRUB_EFI_MMAP_NR_SLACK_SLOTS;
  m = grub_efi_allocate_any_pages (GRUB_EFI_BYTES_TO_PAGES (sizeof(*m) + size));
  if (!m) {
    grub_error (GRUB_ERR_IO, "cannot allocate m memory");
    goto uninstall_initrd_table;
  }

  b = grub_efi_system_table->boot_services;
  status = b->install_configuration_table (&boot_memmap_guid, m);
  if (status != GRUB_EFI_SUCCESS) {
    grub_error (GRUB_ERR_IO, "failed to install boot memmap");
    goto free_m;
  }

  m->buff_size = m->map_size = size;
  if (grub_efi_get_memory_map (&m->map_size, m->map,
                   &m->map_key, &m->desc_size,
                   &m->desc_ver) <= 0)
    {
      grub_error (GRUB_ERR_IO, "cannot get EFI memory map");
      goto uninstall_mem_table;
    }

  mmap_size = grub_efi_find_mmap_size ();
  if (! mmap_size)
    goto uninstall_mem_table;

  mmap_buf = grub_efi_allocate_any_pages (GRUB_EFI_BYTES_TO_PAGES (mmap_size));
  err = grub_efi_finish_boot_services (&mmap_size, mmap_buf, NULL,
                                         &desc_size, &desc_version);
  if (err) {
    grub_error (GRUB_ERR_IO, "failed to finish boot services");
    goto free_map;
  }

  return 0;

free_map:
  if (mmap_buf)
    grub_efi_free_pages ((grub_addr_t) mmap_buf,
               GRUB_EFI_BYTES_TO_PAGES (mmap_size));

uninstall_mem_table:
  b->install_configuration_table (&boot_memmap_guid, NULL);

free_m:
  if (m)
    grub_efi_free_pages ((grub_addr_t) m,
               GRUB_EFI_BYTES_TO_PAGES (sizeof(*m) + size));

uninstall_initrd_table:
  grub_loongarch_remove_initrd_params();

  return grub_error(GRUB_ERR_BAD_OS, "failed to V40 boot");
}

static grub_err_t
allocate_fdt_and_exit_boot (struct linux_loongarch64_kernel_params *kernel_params)
{
  int node, retval;
  grub_err_t err;
  unsigned int size;
  grub_efi_uintn_t mmap_size;
  grub_efi_uintn_t desc_size;
  grub_efi_uint32_t desc_version;
  grub_efi_memory_descriptor_t *mmap_buf;

  size = GRUB_FDT_EMPTY_TREE_SZ + FDT_ADDR_SIZE_EXTRA + GRUB_EFI_LINUX_FDT_EXTRA_SPACE;

  kernel_params->fdt = grub_efi_allocate_any_pages (GRUB_EFI_BYTES_TO_PAGES (size));
  if (!kernel_params->fdt)
    return GRUB_ERR_OUT_OF_MEMORY;

  grub_fdt_create_empty_tree (kernel_params->fdt, size);
  grub_fdt_set_prop32 (kernel_params->fdt, 0, FDT_ADDR_CELLS_STRING, 2);
  grub_fdt_set_prop32 (kernel_params->fdt, 0, FDT_SIZE_CELLS_STRING, 2);

  node = grub_fdt_find_subnode (kernel_params->fdt, 0, "chosen");
  if (node < 0)
    node = grub_fdt_add_subnode (kernel_params->fdt, 0, "chosen");
  if (node < 1)
    goto failure;

  grub_dprintf ("loongson", "command_line %s, len %ld\n",
                (char *)kernel_params->linux_args,
                grub_strlen(kernel_params->linux_args) + 1);
  if ((kernel_params->linux_args != NULL) && (grub_strlen(kernel_params->linux_args) > 0)) {
    retval = grub_fdt_set_prop (kernel_params->fdt, node, "bootargs", kernel_params->linux_args,
				grub_strlen(kernel_params->linux_args) + 1);
    if (retval)
      goto failure;
  }

  /* Set initrd info */
  if (kernel_params->ramdisk_addr && kernel_params->ramdisk_size)
  {
    grub_dprintf ("linux", "Initrd @ %p-%p\n",
		    (void *) kernel_params->ramdisk_addr,
		    (void *) (kernel_params->ramdisk_addr + kernel_params->ramdisk_size));

    retval = grub_fdt_set_prop64 (kernel_params->fdt, node, "linux,initrd-start",
				    kernel_params->ramdisk_addr);
    if (retval)
	  goto failure;
    retval = grub_fdt_set_prop64 (kernel_params->fdt, node, "linux,initrd-end",
		                  (grub_uint64_t) (kernel_params->ramdisk_addr + kernel_params->ramdisk_size));
    if (retval)
	  goto failure;
  }

  node = grub_fdt_find_subnode (kernel_params->fdt, 0, "chosen");
  retval = grub_fdt_set_prop64 (kernel_params->fdt, node, "linux,uefi-system-table",
		                (grub_uint64_t)grub_efi_system_table);
  if (retval)
	goto failure;

  mmap_size = grub_efi_find_mmap_size ();
  if (! mmap_size)
    return grub_errno;
  mmap_buf = grub_efi_allocate_any_pages (GRUB_EFI_BYTES_TO_PAGES (mmap_size));
  if (! mmap_buf)
    return grub_error (GRUB_ERR_IO, "cannot allocate memory map");
  err = grub_efi_finish_boot_services (&mmap_size, mmap_buf, NULL,
                                       &desc_size, &desc_version);
  if (err)
    return err;

  if (!mmap_buf || !mmap_size || !desc_size)
    return GRUB_ERR_BAD_ARGUMENT;

  retval = grub_fdt_set_prop64 (kernel_params->fdt, node, "linux,uefi-mmap-start",
		                (grub_uint64_t)mmap_buf);
  if (retval)
	goto failure;

  retval = grub_fdt_set_prop32 (kernel_params->fdt, node, "linux,uefi-mmap-size",
		                mmap_size);
  if (retval)
	goto failure;

  retval = grub_fdt_set_prop32 (kernel_params->fdt, node, "linux,uefi-mmap-desc-size",
		                desc_size);
  if (retval)
	goto failure;

  retval = grub_fdt_set_prop32 (kernel_params->fdt, node, "linux,uefi-mmap-desc-ver",
		                desc_version);
  if (retval)
	goto failure;

  return GRUB_ERR_NONE;

failure:
  if (!kernel_params->fdt) {
    return GRUB_ERR_BAD_OS;
  }
  grub_efi_free_pages ((grub_addr_t) kernel_params->fdt,
		       GRUB_EFI_BYTES_TO_PAGES (grub_fdt_get_totalsize (kernel_params->fdt)));
  kernel_params->fdt = NULL;
  return grub_error(GRUB_ERR_BAD_OS, "failed to install/update FDT");
}

static void
grub_linux_loongarch_elf_make_argv (struct linux_loongarch64_kernel_params *kernel_params)
{
  static void* linux_args_addr;
  int size;
  grub_uint64_t *linux_argv;
  char *args, *p, *linux_args;
  int i, argc;
  grub_err_t err;

  argc = kernel_params->linux_argc;
  args = kernel_params->linux_args;

  /* new size */
  p = args;
  size = (argc + 3 + 1) * sizeof (grub_uint64_t);  /* orig arguments */
  for (i = 0; i < argc; i++)
    {
      size += ALIGN_UP (grub_strlen (p) + 1, 4);
      p += grub_strlen (p) + 1;
    }

  if (kernel_params->ramdisk_addr && kernel_params->ramdisk_size)
    {
      size += ALIGN_UP (sizeof (GRUB_RD_START_STRING), 4)
	            + ALIGN_UP (sizeof (GRUB_RD_SIZE_STRING), 4)
	            + ALIGN_UP (sizeof (GRUB_INITRD_STRING), 4);
    }
  size = ALIGN_UP (size, 8);

  /* alloc memory */
  linux_args_addr = grub_linux_loongarch_alloc_virtual_mem_align (size, 8, &err);

  linux_argv = linux_args_addr;
  linux_args = (char *)(linux_argv + (argc + 1 + 3));
  p = args;
  for (i = 0; i < argc; i++)
    {
      grub_memcpy (linux_args, p, grub_strlen (p) + 1);
      *linux_argv = (grub_uint64_t) (grub_addr_t) linux_args;
      linux_argv++;
      linux_args += ALIGN_UP (grub_strlen (p) + 1, 4);
      p += grub_strlen (p) + 1;
    }

  if (kernel_params->ramdisk_addr && kernel_params->ramdisk_size)
  {
    /* rd_start */
    grub_snprintf (linux_args,
		 sizeof (GRUB_RD_START_STRING),
		 "rd_start=0x%lx",
		 (grub_uint64_t) kernel_params->ramdisk_addr);
    *linux_argv = (grub_uint64_t) (grub_addr_t) linux_args;
    linux_argv++;
    linux_args += ALIGN_UP (sizeof (GRUB_RD_START_STRING), 4);
    kernel_params->linux_argc++;

    /* rd_size */
    grub_snprintf (linux_args,
		 sizeof (GRUB_RD_SIZE_STRING),
		 "rd_size=0x%lx",
		 (grub_uint64_t) kernel_params->ramdisk_size);
    *linux_argv = (grub_uint64_t) (grub_addr_t) linux_args;
    linux_argv++;
    linux_args += ALIGN_UP (sizeof (GRUB_RD_SIZE_STRING), 4);
    kernel_params->linux_argc++;

    /* initrd */
    grub_snprintf (linux_args,
		 sizeof (GRUB_INITRD_STRING),
		 "initrd=0x%lx,0x%lx",
		 ((grub_uint64_t) kernel_params->ramdisk_addr & 0xffffffff),
		 (grub_uint64_t) kernel_params->ramdisk_size);
    *linux_argv = (grub_uint64_t) (grub_addr_t) linux_args;
    linux_argv++;
    linux_args += ALIGN_UP (sizeof (GRUB_INITRD_STRING), 4);
    kernel_params->linux_argc++;
  }

  /* Reserve space for initrd arguments.  */
  *linux_argv = 0;

  grub_free (kernel_params->linux_args);
  kernel_params->linux_argv = (grub_addr_t) linux_args_addr;
}

grub_err_t
grub_linux_loongarch_elf_linux_boot_image (struct linux_loongarch64_kernel_params
					   *kernel_params)
{
  struct boot_params_interface *boot_params = NULL;
  struct grub_relocator64_state state;
  grub_err_t err;

  /* linux kernel type is ELF */
  grub_memset (&state, 0, sizeof (state));

  state.jumpreg = 1;
  state.gpr[1] = kernel_params->kernel_addr;  /* ra */
  if (grub_linux_loongarch_elf_get_boot_params (&boot_params) == 0)
    {
      grub_dprintf("loongson", "V4.0 boot\n");
      if (allocate_memmap_and_exit_boot (kernel_params) != GRUB_ERR_NONE)
        return grub_errno;
      state.gpr[4] = 1 << FLAGS_EFI_SUPPORT_BIT;   /* a0 = flag */
      state.gpr[5] = (grub_uint64_t)kernel_params->linux_args; /* a1 = cmdline */
      state.gpr[6] = (grub_uint64_t)grub_efi_system_table; /* a2 = system_table */
    } else {
      grub_dprintf("loongson", "BPI boot\n");
      grub_linux_loongarch_elf_make_argv (kernel_params);
      state.gpr[4] = kernel_params->linux_argc;   /* a0 = argc */
      state.gpr[5] = kernel_params->linux_argv; /* a1 = args */
      state.gpr[6] = (grub_uint64_t) boot_params; /* a2 = envp */
      err = grub_linux_loongarch_elf_boot_params (boot_params);
      if (err)
        return err;
    }

  /* Boot the ELF kernel */
  grub_relocator64_boot (relocator, state);

  return GRUB_ERR_NONE;
}

void*
grub_linux_loongarch_alloc_virtual_mem_addr (grub_addr_t addr,
					     grub_size_t size,
					     grub_err_t *err)
{
  relocator = grub_relocator_new ();
  if (!relocator)
    return NULL;

  grub_relocator_chunk_t ch;
  *err = grub_relocator_alloc_chunk_addr (relocator, &ch,
					 grub_vtop ((void *) addr),
					 size);
  if (*err)
    return NULL;
  return get_virtual_current_address (ch);
}

void*
grub_linux_loongarch_alloc_virtual_mem_align (grub_size_t size,
					      grub_size_t align,
					      grub_err_t *err)
{
  grub_relocator_chunk_t ch;

  *err = grub_relocator_alloc_chunk_align (relocator, &ch,
					  0, (0xffffffff - size) + 1,
					  size, align,
					  GRUB_RELOCATOR_PREFERENCE_LOW, 0);
  return get_virtual_current_address (ch);
}

void*
grub_linux_loongarch_alloc_initrd_mem_align (grub_size_t size,
                 grub_size_t align,
                 grub_err_t *err)
{
  grub_relocator_chunk_t ch;

  /* Firstly try to allocate from memory higher than 256MB */
  *err = grub_relocator_alloc_chunk_align (relocator, &ch,
              0x10000000, (0xffffffff - size) + 1, size, align,
              GRUB_RELOCATOR_PREFERENCE_LOW, 0);
  if (*err != GRUB_ERR_NONE)
    {
      /* Failed, try to allocate in range 0 ~ 256MB */
      *err = grub_relocator_alloc_chunk_align (relocator, &ch,
                  0, (0xfffffff - size) + 1, size, align,
                  GRUB_RELOCATOR_PREFERENCE_HIGH, 0);
    }
  return get_virtual_current_address (ch);
}

int
grub_linux_loongarch_elf_get_boot_params (struct boot_params_interface **boot_params)
{
  grub_efi_configuration_table_t *tables;
  grub_guid_t bpi_guid = GRUB_EFI_LOONGSON_BPI_TABLE_GUID;
  unsigned int i;
  int found = 0;

  /* Look for Loongson BPI in UEFI config tables. */
  tables = grub_efi_system_table->configuration_table;

  for (i = 0; i < grub_efi_system_table->num_table_entries; i++)
    if (grub_memcmp (&tables[i].vendor_guid, &bpi_guid, sizeof (bpi_guid)) == 0)
      {
	*boot_params = tables[i].vendor_table;
	char *p = (char*) &((*boot_params)->signature);
	if (grub_strncmp (p, "BPI", 3) == 0)
	  {
	    found = 1;
	    break;
	  }
      }
  return found;
}

static grub_uint8_t
grub_kernel_update_checksum (const grub_uint8_t *buffer, grub_efi_uintn_t length)
{
  grub_uint8_t sum;
  grub_efi_uintn_t count;

  for (sum = 0, count = 0; count < length; count++)
  {
    sum = (grub_uint8_t) (sum + *(buffer + count));
  }

  return (grub_uint8_t) (0x100 - sum);
}

static grub_uint32_t
grub_efi_loongarch64_memmap_sort (struct memmap array[],
				  grub_uint32_t length,
				  struct loongsonlist_mem_map* bpmem,
				  grub_uint32_t index,
				  grub_uint32_t memtype)
{
  grub_uint64_t tempmemsize = 0;
  grub_uint32_t j = 0;
  grub_uint32_t t = 0;

  for(j = 0; j < length;)
  {
    tempmemsize = array[j].mem_size;
    for(t = j + 1; t < length; t++)
    {
      if(array[j].mem_start + tempmemsize == array[t].mem_start)
      {
        tempmemsize += array[t].mem_size;
      }
      else
      {
        break;
      }
    }
    bpmem->map[index].mem_type = memtype;
    bpmem->map[index].mem_start = array[j].mem_start;
    bpmem->map[index].mem_size = tempmemsize;
    grub_printf("map[%d]:type %"PRIuGRUB_UINT32_T", start 0x%"
		PRIxGRUB_UINT64_T", end 0x%"PRIxGRUB_UINT64_T"\n",
		index,
		bpmem->map[index].mem_type,
		bpmem->map[index].mem_start,
		bpmem->map[index].mem_start+ bpmem->map[index].mem_size
	       );
    j = t;
    index++;
  }
  return index;
}

grub_err_t
grub_linux_loongarch_elf_boot_params (struct boot_params_interface *boot_params)
{
  grub_int8_t checksum = 0;
  grub_err_t err;

  struct loongsonlist_mem_map *loongson_mem_map = NULL;
  struct _extention_list_hdr * listpointer = NULL;
  grub_uint32_t tmp_index = 0;
  grub_efi_memory_descriptor_t * lsdesc = NULL;

  grub_uint32_t free_index = 0;
  grub_uint32_t reserve_index = 0;
  grub_uint32_t acpi_table_index = 0;
  grub_uint32_t acpi_nvs_index = 0;

  grub_efi_uintn_t mmap_size;
  grub_efi_uintn_t desc_size;
  grub_efi_memory_descriptor_t *mmap_buf;

  struct memmap reserve_mem[GRUB_LOONGSON3_BOOT_MEM_MAP_MAX];
  struct memmap free_mem[GRUB_LOONGSON3_BOOT_MEM_MAP_MAX];
  struct memmap acpi_table_mem[GRUB_LOONGSON3_BOOT_MEM_MAP_MAX];
  struct memmap acpi_nvs_mem[GRUB_LOONGSON3_BOOT_MEM_MAP_MAX];

  grub_memset (reserve_mem, 0, sizeof(struct memmap) * GRUB_LOONGSON3_BOOT_MEM_MAP_MAX);
  grub_memset (free_mem, 0, sizeof(struct memmap) * GRUB_LOONGSON3_BOOT_MEM_MAP_MAX);
  grub_memset (acpi_table_mem, 0, sizeof(struct memmap) * GRUB_LOONGSON3_BOOT_MEM_MAP_MAX);
  grub_memset (acpi_nvs_mem, 0, sizeof(struct memmap) * GRUB_LOONGSON3_BOOT_MEM_MAP_MAX);

  /* Check extlist headers */
  listpointer = boot_params->extlist;
  for( ;listpointer != NULL; listpointer = listpointer->next)
    {
      char *pl= (char *)&(listpointer->signature);
      if(grub_strncmp(pl, "MEM", 3) == 0)
        {
          loongson_mem_map = (struct loongsonlist_mem_map *)listpointer;
          break;
        }
    }

  mmap_size = grub_efi_find_mmap_size ();
  if (! mmap_size)
    return grub_errno;
  mmap_buf = grub_efi_allocate_any_pages (GRUB_EFI_BYTES_TO_PAGES (mmap_size));
  if (! mmap_buf)
    return grub_error (GRUB_ERR_IO, "cannot allocate memory map");

  err = grub_efi_finish_boot_services (&mmap_size, mmap_buf, NULL,
				       &desc_size, NULL);
  if (err)
    return err;

  if (!mmap_buf || !mmap_size || !desc_size)
    return -1;

  /*
     According to UEFI SPEC,mmap_buf is the accurate Memory Map array \
     now we can fill platform specific memory structure.
     */
  for (lsdesc = mmap_buf; lsdesc < (grub_efi_memory_descriptor_t *)((char *)mmap_buf + mmap_size);
      lsdesc = (grub_efi_memory_descriptor_t *)((char *)lsdesc + desc_size))
    {
      /* System RAM */
      if((lsdesc->type != GRUB_EFI_ACPI_RECLAIM_MEMORY) && \
	 (lsdesc->type != GRUB_EFI_ACPI_MEMORY_NVS) && \
	 (lsdesc->type != GRUB_EFI_RUNTIME_SERVICES_DATA) && \
	 (lsdesc->type != GRUB_EFI_RUNTIME_SERVICES_CODE) && \
	 (lsdesc->type != GRUB_EFI_RESERVED_MEMORY_TYPE) && \
	 (lsdesc->type != GRUB_EFI_PAL_CODE))
	{
	  free_mem[free_index].mem_type = GRUB_ADDRESS_TYPE_SYSRAM;
	  free_mem[free_index].mem_start = (lsdesc->physical_start) & GRUB_EFI_MAX_PHY_ADDRESS;
	  free_mem[free_index].mem_size = lsdesc->num_pages * GRUB_EFI_PAGE_SIZE;
	  free_index++;

	  /*ACPI*/
	}else if((lsdesc->type == GRUB_EFI_ACPI_RECLAIM_MEMORY)){
	    acpi_table_mem[acpi_table_index].mem_type = GRUB_ADDRESS_TYPE_ACPI;
	    acpi_table_mem[acpi_table_index].mem_start = (lsdesc->physical_start) & GRUB_EFI_MAX_PHY_ADDRESS;
	    acpi_table_mem[acpi_table_index].mem_size = lsdesc->num_pages * GRUB_EFI_PAGE_SIZE;
	    acpi_table_index++;
	}else if((lsdesc->type == GRUB_EFI_ACPI_MEMORY_NVS)){
	    acpi_nvs_mem[acpi_nvs_index].mem_type = GRUB_ADDRESS_TYPE_NVS;
	    acpi_nvs_mem[acpi_nvs_index].mem_start = (lsdesc->physical_start) & GRUB_EFI_MAX_PHY_ADDRESS;
	    acpi_nvs_mem[acpi_nvs_index].mem_size = lsdesc->num_pages * GRUB_EFI_PAGE_SIZE;
	    acpi_nvs_index++;

	/* Reserve */
	}else{
	    reserve_mem[reserve_index].mem_type = GRUB_ADDRESS_TYPE_RESERVED;
	    reserve_mem[reserve_index].mem_start = (lsdesc->physical_start) & GRUB_EFI_MAX_PHY_ADDRESS;
	    reserve_mem[reserve_index].mem_size = lsdesc->num_pages * GRUB_EFI_PAGE_SIZE;
	    reserve_index++;
	}
    }

  tmp_index = loongson_mem_map->map_count;
  /*System RAM Sort*/
  tmp_index = grub_efi_loongarch64_memmap_sort(free_mem,
						free_index,
						loongson_mem_map,
						tmp_index,
						GRUB_ADDRESS_TYPE_SYSRAM);
  /*ACPI Sort*/
  tmp_index = grub_efi_loongarch64_memmap_sort(acpi_table_mem,
						acpi_table_index,
						loongson_mem_map,
						tmp_index,
						GRUB_ADDRESS_TYPE_ACPI);
  tmp_index = grub_efi_loongarch64_memmap_sort(acpi_nvs_mem,
						acpi_nvs_index,
						loongson_mem_map,
						tmp_index,
						GRUB_ADDRESS_TYPE_NVS);

  /*Reserve Sort*/
  {
    grub_uint64_t loongarch_addr;
    asm volatile ("csrrd %0, 0x181" : "=r" (loongarch_addr));
    if ((loongarch_addr & 0xff00000000000000) == 0x9000000000000000)
      tmp_index = grub_efi_loongarch64_memmap_sort(reserve_mem,
						reserve_index,
						loongson_mem_map,
						tmp_index,
						GRUB_ADDRESS_TYPE_RESERVED);
    else
      tmp_index = grub_efi_loongarch64_memmap_sort(reserve_mem,
						reserve_index,
						loongson_mem_map,
						tmp_index,
						GRUB_ADDRESS_TYPE_RESERVED + 1);
  }
  loongson_mem_map->map_count = tmp_index;
  loongson_mem_map->header.checksum = 0;

  checksum = grub_kernel_update_checksum ((grub_uint8_t *) loongson_mem_map,
					  loongson_mem_map->header.length);
  loongson_mem_map->header.checksum = checksum;

  return grub_errno;
}
